/**

This file is part of MaCI/GIMnet.

MaCI/GIMnet is free software: you can redistribute it and/or modify it 
under the terms of the GNU Lesser General Public License as published 
by the Free Software Foundation, either version 3 of the License, or 
(at your option) any later version.

MaCI/GIMnet is distributed in the hope that it will be useful, but WITHOUT 
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 
FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public 
License for more details.

You should have received a copy of the GNU Lesser General Public 
License along with GIMnet. (See COPYING.LESSER) If not, see 
<http://www.gnu.org/licenses/>.

**/
/**
 *
 * $Id: launcher.cpp,v 1.32 2009-05-13 07:18:19 amaula Exp $
 * 
 * \file
 * \brief Implementation file for Launcher class.
 * \author Antti Maula <antti.maula@tkk.fi>
 *
 */
#include "launcher.hpp"
#include "directory.hpp"
#include "owndebug.h"
#include "ownutils.h"
#include "configreader.hpp"
#include <assert.h>
#include <fcntl.h>

//*****************************************************************************
// For easier calls to dPrintLC
static const unsigned char d_red[3] = { 
  OWNDEBUG_COLOR_ATTR_BRIGHT, OWNDEBUG_COLOR_RED, OWNDEBUG_COLOR_BLACK 
};
static const unsigned char d_dimred[3] = { 
  OWNDEBUG_COLOR_ATTR_DIM, OWNDEBUG_COLOR_RED, OWNDEBUG_COLOR_BLACK 
};
static const unsigned char d_wred[3] = { 
  OWNDEBUG_COLOR_ATTR_BRIGHT, OWNDEBUG_COLOR_WHITE, OWNDEBUG_COLOR_RED 
};
static const unsigned char d_green[3] = { 
  OWNDEBUG_COLOR_ATTR_BRIGHT, OWNDEBUG_COLOR_GREEN, OWNDEBUG_COLOR_BLACK 
};
//*****************************************************************************

CLauncher::CLauncher()
  : iMachineActive(false),
    iMachineTerminating(false),
    iActiveModules(),
    iMachineConfig(),
    iModuleConfigArray(),
    iGIMnet_host(),
    iGIMnet_port(0)
{
}
//*****************************************************************************

CLauncher::~CLauncher()
{
  int r = 0;
  TerminateMachine();
  WaitMachine(r);
  dPrint(4,"Launcher destructor; WaitMachine(r) resulted in r=%d", r);
}
//*****************************************************************************

bool CLauncher::BuildMachine(const std::string &aMachinePath)
{
  bool r,result = false;
  SMachineConfEntry machineConf;
  TModuleConfEntryArray moduleConfArray;

  // Check for root priv.
  if (geteuid() == 0) {
    int uid = getuid(); 
    int gid = getgid();
    GetNonRootCredentials(uid, gid);
    int sv = setgid(gid);
    sv |= setuid(uid);
    if (!sv) {
      dPrint(ODVINFO,"Executing with uid/gid = %d/%d", uid, gid);
      
    } else {
      dPrint(ODINFO,"Failed to set uid/gid to %d/%d, but still going", 
             uid, gid);

    }

  }
  
  // Read machine configuration
  dPrint(3,"Reading Machine configuration...");
  r = ReadMachine(aMachinePath, machineConf, moduleConfArray);
  if (r) {
    
    dPrint(3,"Configuration read OK.");
    // Conf read OK. Now Run the modules!
    
    // Mark operation to be succesfull, until otherwise marked
    result = true;
    
    for(EACH_IN_i(moduleConfArray)) {
      if (i->buildCommand.size()) {
        dPrintLC(1, d_green[0], d_green[1], d_green[2],
                 "Building module '%s' using command: '%s'",
                 i->name.c_str(), i->buildCommand.c_str());
        
        int v = system(i->buildCommand.c_str());
        if (v == -1) {
          dPrintLC(1, d_wred[0], d_wred[1], d_wred[2], 
                   "Failed to execute system('%s')!", i->buildCommand.c_str());
          
        } else if (WEXITSTATUS(v) == 0) {
          dPrintLC(1, d_green[0], d_green[1], d_green[2],
                   "Build command for module '%s' terminated with status %d", i->name.c_str(), 
                   WEXITSTATUS(v));
          
        } else {
          dPrintLC(1, d_wred[0], d_wred[1], d_wred[2], 
                   "Build command for module '%s' terminated with status %d", i->name.c_str(), 
                   WEXITSTATUS(v));
          result = false;
        }
        
      } else {
        dPrint(1,"No Build-command available for module: '%s', please add it to use this feature!",
               i->name.c_str());
      }
    }
  }

  return result;
}  
//*****************************************************************************

bool CLauncher::ExecuteMachine(const std::string &aMachinePath)
{
  bool r,result = false;
  SMachineConfEntry machineConf;
  TModuleConfEntryArray moduleConfArray;

  if (iMachineActive == false) {
    iMachineTerminating = false;
    
    // Machine not running; so there should be no active modules!
    assert(iActiveModules.size() == 0);
    
    // Read machine configuration
    dPrint(3,"Reading Machine configuration...");
    r = ReadMachine(aMachinePath, machineConf, moduleConfArray);
    if (r) {

      dPrint(3,"Configuration read OK.");
      // Conf read OK. Now Run the modules!
      bool exec_status = true;
      for(EACH_IN_i(moduleConfArray)) {
        CExecutor *exe = ExecuteModule(machineConf, *i);
        if (exe) {
          dPrint(1,"Succesfully executed module: '%s'", i->name.c_str());
          iActiveModules.push_back(exe);
          
        } else {
          dPrint(1,"Failed to Run module: '%s'", i->name.c_str());
          exec_status = false;
          break;
        }
      }

      // Check final status
      if (exec_status) {
        dPrint(1,"Succesfully started machine: '%s' (%d modules/extbins activated)",
               machineConf.name.c_str(),
               iActiveModules.size());
        result = true;
        iMachineActive = true;

        // Store configurations inside class.
        iMachineConfig = machineConf;
        iModuleConfigArray = moduleConfArray;

      } else {
        dPrint(1,"Failed to start machine '%s'!",
               machineConf.name.c_str());
        
      }
    }
  }

  return result;
}
//*****************************************************************************

bool CLauncher::TerminateMachine()
{
  if (iMachineActive) {
    dPrint(1,"Terminating Machine...");
    iMachineTerminating = true;

    dPrint(2,"Sending SIGTERM to all Module processes...");
    // Be nice and send 'SIGTERM' to all modules
    for(EACH_IN_i(iActiveModules)) {
      CExecutor *e = *i;
      if (e) e->Terminate();
    }
    
    // Wait for one second
    // \todo Make the Sleeptime between TERM and KILL configurable.
    ownSleep_ms(3000);

    dPrint(2,"Sending SIGKILL to all Module processes...");
    // Finish all remaining suckers off with SIGKILL
    for(EACH_IN_i(iActiveModules)) {
      CExecutor *e = *i;
      if (e) e->Kill();
    }
    return true;
  }
  return false;
}
//*****************************************************************************

bool CLauncher::WaitMachine(bool aBlock)
{
  bool result = false;
  bool clear = true;
  int r = 0;
  std::string id;

  if (iMachineActive) {
    dPrint(6, "Waiting for Machine to completete...");

    int index = 0;
    for(EACH_IN_i(iActiveModules)) {
      CExecutor *e = *i;
      if (e != NULL) {
        // Set clear to 'false', as there are still active modules. (e != NULL);
        clear = false;
        
        // Check whether we have name available, otherwise use ID.
        if (iModuleConfigArray[index].name.size()) {
          id = iModuleConfigArray[index].name;
        } else {
          char num[32];
          sprintf(num,"%u", e->GetProcessID());
          id = num;
        }


        // Do it
        const int pid = e->GetProcessID();
        int value;
        r = e->Wait(value, aBlock);
        if (r == pid) {
          // r == pid
          if (WIFEXITED(value)) {
            dPrintLC(1, d_red[0], d_red[1], d_red[2], 
                     "Process '%s' (pid %d) exited normally with exit status '%d'",
                     id.c_str(), pid, WEXITSTATUS(value));
            
          } else if (WIFSIGNALED(value)) {
            dPrintLC(1, d_wred[0], d_wred[1], d_wred[2],
                     "Process '%s' (pid %d) exited due to uncaught signal '%d' (%s)",
                     id.c_str(), pid, WTERMSIG(value), strsignal(WTERMSIG(value)));
          }

          // Now, check for restart status.
          const SModuleConfEntry &modconf = iModuleConfigArray[index];
          if (modconf.flags & KModuleFlagRespawnProcess && iMachineTerminating == false) {
            // Re-Execute requested!
            if (e->ReExecute(modconf.respawnTime_ms)) {
              dPrintLC(1,d_green[0],d_green[1],d_green[2],
                       "Succesfully Re-Executed process '%s' (pid %d) (Extra delay %ums before real execution)", 
                       id.c_str(), pid, modconf.respawnTime_ms);
              
            } else {
              dPrintLC(2,d_dimred[0], d_dimred[1], d_dimred[2],
                       "Re-Executing process '%s' (pid %d) failed! Will now delete it.", 
                       id.c_str(), pid);
              // Now delete.
              delete e;
              *i = NULL;
            }

          } else {
            // No respawn requested. Delete & mark NULL.
            delete e;
            *i = NULL;

          }

        } else if (r == 0) {
          // r == 0 and block
          dPrint(15,"No status available for process '%s' (pid %d) (Process still active)",
                 id.c_str(), pid);
          
        } else if (r < 0) {
          dPrintLE(1,"Failed to Wait() for process '%s' (pid %d)!", 
                   id.c_str(), pid);
          
        }
      }
      ++index;
    }

    // Check whether clear flag is set, and act upon.
    if (clear) {
      iActiveModules.clear();
      iMachineActive = false;
      dPrintLC(1, d_green[0],d_green[1],d_green[2],
               "Machine has terminated.");
      result = true;
    }
  }
  return result;
}
//*****************************************************************************

void CLauncher::ReadAndPrintMachineOutput(const int aLevel, const unsigned int aTimeout_ms)
{
  const unsigned int tbegin = ownTime_get_ms();
  int tleft = aTimeout_ms;
  char mbuffer[4096];
  char line[512];
  char outputline[640];
  FILE *prints = NULL, *printl = NULL;

  if (iMachineActive) {
    unsigned int index = 0;
    for(EACH_IN_i(iActiveModules)) {
      CExecutor *e = *i;

      if (e != NULL && e->WaitReadIOPipe(tleft>0?tleft:0) > 0) {
        int b = e->ReadIOPipe(mbuffer, sizeof(mbuffer));
        dPrint(15,"Got %d bytes of output from process %d", 
               b, e->GetProcessID());

        // First, take strem pointers according to current level
        if (debugGetPrintLevel() >= aLevel) prints = debugGetPrintStream();
        if (debugGetLogLevel() >= aLevel) printl = debugGetLogStream();
        
        // Test whether either one was non-null.
        if (prints != NULL || 
            printl != NULL) {
          std::string id;
           
          // Check whether we have name available, otherwise use ID.
          if (iModuleConfigArray[index].name.size()) {
            id = iModuleConfigArray[index].name;
          } else {
            char num[32];
            sprintf(num,"%u",e->GetProcessID());
            id = num;
          }
          
          // Extract lines from the read-in buffer and print them
          for(int i=0,o=0; i < b && o < (int)sizeof(line); ++i, ++o) {
            line[o] = mbuffer[i];
            
            if (line[o] == '\n' || i == b-1) {
              
              // If no terminating '\n' and last char.
              if (i == b-1 && line[o] != '\n') ++o;
              
              // Insert terminating NULL.
              line[o] = 0;
              
              // Format the line
              int lb = snprintf(outputline,
                                sizeof(outputline),
                                "==%s==: %s\n", 
                                id.c_str(),
                                line);
              if (lb > 0) {
                // Write out the formatted string effectively
                if (prints != NULL) fwrite(outputline, lb, 1, prints);
                if (printl != NULL) fwrite(outputline, lb, 1, printl);
              } else {
                dPrintLE(1,"snprintf() returned -1?");
                
              }
              o = -1;
            }
            
          }
        }
      }
      
      // Increment index.
      ++index;

      // Update time-left
      tleft = aTimeout_ms - ownTime_get_ms_since(tbegin); 
    }
  }
}
//*****************************************************************************

void CLauncher::SetDefaultGIMnetAPHost(const std::string &aDefaultHost)
{
  iGIMnet_host = aDefaultHost;
}
//*****************************************************************************

void CLauncher::SetDefaultGIMnetAPPort(const unsigned int &aDefaultPort)
{
  iGIMnet_port = aDefaultPort;
}
//*****************************************************************************
//*****************************************************************************
//*****************************************************************************
//*****************************************************************************
//*****************************************************************************
//*****************************************************************************
//*****************************************************************************
//*****************************************************************************
//*****************************************************************************
//*****************************************************************************

void CLauncher::GetNonRootCredentials(int &new_uid, int &new_gid)
{
  // Do we have root privileges? If so, drop them!
  if (geteuid() == 0) {
    if (getuid() == 0) {
      // Real userid is root. Check whether we are run under SUDO.
      const char *sudouidstr = getenv("SUDO_UID");
      const char *sudogidstr = getenv("SUDO_GID");
      if (sudouidstr) {
        dPrint(4,"Got SUDO_UID='%s', attempting to use it as UID for the module",
               sudouidstr);
        new_uid = atoi(sudouidstr);

      } else {
        dPrint(4,"No SUDO_UID available, running as uid = root!");

      }

      if (sudogidstr) {
        dPrint(4,"Got SUDO_GID='%s', attempting to use it as GID for the module",
               sudogidstr);
        new_gid = atoi(sudogidstr);

      } else {
        dPrint(4,"No SUDO_GID available, running as gid = root!");
        
      }
      
    } else {
      // Real userid is not root -> set it.
      new_uid = getuid();
      new_gid = getgid();
      
    }
  }
}
//*****************************************************************************

CExecutor *CLauncher::ExecuteModule(const SMachineConfEntry &aMachineConfig,
                                    const SModuleConfEntry &aModuleConfig)
{
  CExecutor *exe = new CExecutor();
  TStringArray param;
  
  // Specials
  const int start_delay = aModuleConfig.startdelay;

  // Get GIMnet parameters.
  const std::string ghost = GetModuleGIMnetHost(aMachineConfig, aModuleConfig);
  const std::string gport = GetModuleGIMnetPort(aMachineConfig, aModuleConfig);
  const std::string gname = GetModuleName(aMachineConfig, aModuleConfig);
  const std::string mgname = GetMaCIGroupName(aMachineConfig, aModuleConfig);
  std::string working_directory; // Working directory, defaults to empty.
  int uid = -1, gid = -1; // UID/GID to run as. (-1 is no change)


  // If not set to ignore, push GIMnet param.
  if (!(aModuleConfig.flags & KModuleFlagIgnoreGIMnetParam)) {
    // Push parameters for the soon-to-be-called executable
    param.push_back("-u"); // Host setting
    param.push_back(ghost);
    param.push_back("-p"); // Port setting
    param.push_back(gport);
    param.push_back("-n"); // name setting
    param.push_back(gname);
  }

  // If not set to ignore, push MaCI param.
  if (!(aModuleConfig.flags & KModuleFlagIgnoreMaCIParam)) {
    if (mgname.size()) {
      param.push_back("-G");
      param.push_back(mgname);
    }
  }

  // If not set to ignore, push ModuleConfig.
  if (!(aModuleConfig.flags & KModuleFlagIgnoreModuleConfigParam)) {
    // If moduleconfig is set, set it here.
    if (aModuleConfig.moduleConf.size()) {
      param.push_back("-f");
      param.push_back(aModuleConfig.moduleConf);
    }
  }

  // If not set to ignore, push extra param.
  if (!(aModuleConfig.flags & KModuleFlagIgnoreExtraParam)) {
    // Add module specific parameters
    for(EACH_IN_i(aModuleConfig.moduleParam)) {
      param.push_back(*i);
    }
  }

  // Check for root privileges requirement
  if (aModuleConfig.flags & KModuleFlagDropRootPrivileges) {

    // If Drop root is set, attempt to get Non-root uid/gid.
    GetNonRootCredentials(uid, gid);
  }

  // Debug (Print all parameters in stack)
  for(EACH_IN_i(param)) {
    dPrint(15,"Param: '%s'", i->c_str());
  }

  // Create IO Pipe.
  exe->CreateIOPipe();

  // If WorkingDir is not ignored, set it to modules own.  If it is
  // empty, nothing of course happens :)
  if (!(aModuleConfig.flags & KModuleFlagIgnoreWorkingDir)) {
    working_directory = aModuleConfig.workingDirectory;
  }

  // Execute!!!
  bool r = exe->Execute(aModuleConfig.executablePath,
                        param, 
                        start_delay,
                        working_directory,
                        uid, gid);
  if (r) {
    dPrint(2,"Succesfully executed '%s' (Got PID %d)",
           aModuleConfig.executablePath.c_str(),
           exe->GetProcessID());
  } else {
    dPrint(2,"Failed to Execute '%s'! Deleting CExecutor instance.",
           aModuleConfig.executablePath.c_str());
    
    delete exe;
    exe = NULL;
  }

  return exe;
}
//*****************************************************************************

TStringArray CLauncher::GetConfsByPattern(const std::string &aDir, 
                                          const std::string &aPattern)
{
  CDirectory d;
  TStringArray confs;

  // Read directory.
  bool r = d.Open(aDir);
  if (!r) {
    dPrint(1,"Failed to open workdir: '%s'",
           aDir.c_str());
    
  } else {
    // Open OK, now get files.
    r = d.GetFileList(confs, aPattern);
    if (!r) {
      dPrint(3, "Failed to get FileList for machine directory. Abort!");
      confs.clear();

    } else {
      dPrint(8,"Got %d confs by pattern", confs.size());
    }
  }

  return confs;
}
//*****************************************************************************

std::string CLauncher::GetModuleGIMnetHost(const SMachineConfEntry &aMachineConfig,
                                           const SModuleConfEntry &aModuleConfig)
{
  // Select the used GIMnet host
  if (aModuleConfig.GIMnet_host.size()) {
    return aModuleConfig.GIMnet_host;
  } else if (aMachineConfig.GIMnet_host.size()) {
    return aMachineConfig.GIMnet_host;
  } else {
    return iGIMnet_host;
  }
}
//*****************************************************************************

std::string CLauncher::GetModuleGIMnetPort(const SMachineConfEntry &aMachineConfig,
                                           const SModuleConfEntry &aModuleConfig)
{
  int p = 0;
  char port[32];
  // Select the used GIMnet port.
  if (aModuleConfig.GIMnet_port) {
    p = aModuleConfig.GIMnet_port;
  } else if (aMachineConfig.GIMnet_port) {
    p = aMachineConfig.GIMnet_port;
  } else {
    p = iGIMnet_port;
  }
  sprintf(port,"%d", p);
  return port;
}
//*****************************************************************************

std::string CLauncher::GetModuleName(const SMachineConfEntry &aMachineConfig,
                                     const SModuleConfEntry &aModuleConfig)
{
  std::string name;
  
  if (aModuleConfig.GIMnet_name.size()) {
    // Set the name specified by module config.
    name = aModuleConfig.GIMnet_name;
  } else {
    // Do nada :)
  }
  
  return name;
}
//*****************************************************************************

std::string CLauncher::GetMaCIGroupName(const SMachineConfEntry &aMachineConfig,
                                        const SModuleConfEntry &aModuleConfig)
{
  std::string name;
  
  if (aModuleConfig.MaCIGroup.size()) {
    // Set the name specified by module config.
    name = aModuleConfig.MaCIGroup;

  } else if (aMachineConfig.MaCIGroup.size()) {
    name = aMachineConfig.MaCIGroup;

  } else {
    // Do nada.
  }
  
  return name;
}
//*****************************************************************************

std::string CLauncher::FindMachineConfig(const std::string &aMachinePath)
{
  std::string result;
  if (CDirectory::IsRegular(aMachinePath)) {
    // Parameter is regular file. Assume it is a Machine configuration file.
    result = aMachinePath;

  } else {
    CDirectory d;
    
    dPrint(1,"Given Machine is not a regular file. Scanning for '*.mconf' files...");
 
    // Read directory.
    bool r = d.Open(aMachinePath);
    if (!r) {
      dPrint(1,"Provided Machine was not Regular file and we failed to open it as directory: '%s'.",
             aMachinePath.c_str());
      
    } else {
      TStringArray confs;

      // Open OK, now get files.
      dPrint(8,"Succesfully opened machine directory. Reading '*.mconf' files.");
      r = d.GetFileList(confs, ".*\\.mconf$");
      if (!r) {
        dPrint(1, "Failed to get FileList for machine directory. Abort!");

      } else if (confs.size() == 1) {
        dPrint(1, "1 match found ('%s') Assuming it is correct.", 
               confs[0].c_str());
        result = confs[0];

      } else if (confs.size() > 1) { 
        dPrint(1,"Warning: Multiple '*.mconf' files found (%d). Using first found: '%s'",
               confs.size(), confs[0].c_str());
        result = confs[0];

      } else {
        dPrint(1,"No configurations found. This probably isn't a MaCI Machine directory.");
        
      }
    }
  }
  
  return result;
}
//*****************************************************************************

bool CLauncher::ReadAllModules(TModuleConfEntryArray &aArray, const SMachineConfEntry &aMachine)
{
  // Clear first.
  aArray.clear();

  // Container for configs
  TStringArray configs;
  
  // Insert all explicit configurations.
  configs.insert(configs.end(), aMachine.moduleFiles.begin(), aMachine.moduleFiles.end());
  
  // Insert all by pattern.
  for(EACH_IN_i(aMachine.modulePatterns)) {
    TStringArray a = GetConfsByPattern(".", *i);
    dPrint(2,"Got %d Configuration files by pattern: '%s'", a.size(), i->c_str());
    configs.insert(configs.end(), a.begin(), a.end());
  }                        
      
  // now, extract the Module configs
  bool result = true;
  SModuleConfEntry modco;
  for(EACH_IN_i(configs)) {
    modco.Clear();
    result &= CConfigReader::ReadConfig(modco, *i);
    
    // Check result.
    if (result) {
      aArray.push_back(modco);
    } else {
      break;
    }
  }
  
  return result;
}
//*****************************************************************************

bool CLauncher::ReadMachine(const std::string &aMachinePath, 
                            SMachineConfEntry &aMachineConf, 
                            TModuleConfEntryArray &aModuleConfArray)
{
  bool result = false;
  std::string wd;
  std::string machineConfig;
  TStringArray confs;
  SMachineConfEntry mce;
  TModuleConfEntryArray moduleconfs;

  // This returns the mconf file if any is available.
  machineConfig = FindMachineConfig(aMachinePath);

  // Parse Machine Config
  if (CConfigReader::ReadConfig(mce, machineConfig) == true) {
    // Succesfully parsed machine config,
    
    // Set working directory
    if (mce.workdir.size()) {
      // If set by machineconfig..
      wd = mce.workdir;
    } else {
      // Not set, so attempt to take the dir part of aMachinePart

      if (CDirectory::IsRegular(aMachinePath)) {
        // If the file is regular, use the DIR part of 
        wd = CDirectory::GetDirname(aMachinePath);
      } else {
        wd = aMachinePath;
      }
    }

    // Change to desired working directory.
    int r = chdir(wd.c_str());
    if (r != 0) {
      dPrintLE(1,"Unable to change Working directory to: '%s'",
               wd.c_str());

    } else {
      dPrint(3,"Succesfully changed working directory to: '%s'",
             wd.c_str());

      // Call read for extbins.
      r = ReadAllModules(moduleconfs, mce);

      // Check whether reading configs was ok.
      if (r) {
        dPrint(3,"Succesfully read all Module configs.");
        aMachineConf = mce;
        aModuleConfArray = moduleconfs;
        result = true;
      }
    }
  }
 
  return result;
}

//*****************************************************************************
//*****************************************************************************
